#!/usr/bin/env python

# Defaults
bfile = 'Buildfile.py'
target = 'all'

notice = """
Build Tool, 0.93
	copyright 2006 - all rights reserved
	Russell Y. Webb - rwebb@nthlab.com
-------------------------------------------------------

usage: ./build [target] [Options]

target defaults to '%s' if not provided.
Most build files will contain 'all' and 'clean'
""" % target

# todo: would be nice for the help message to know about all possible targets in the buildfile

import os
import commands
import re
import sys
from optparse import OptionParser

# run a command and return a list of status and output
# encapsulated here because getstatusoutput might not be available on all platforms
def Run(cmd, verbose = False, show_reply = True):
	outcome = commands.getstatusoutput(cmd)
	if verbose or outcome[0] != 0:
		print(cmd)
	if outcome[0] != 0 or (show_reply and outcome[1] != ''):
		print(outcome[1])
	if outcome[0] != 0:
		sys.exit(0)
	return outcome[1]

def Shell(cmd):
	outcome = commands.getstatusoutput(cmd)
	if outcome[0] != 0:
		print(cmd)
		print(outcome[1])
		sys.exit(0)
	return outcome[1]

# does a file exist
def Exists(path):
	return os.path.exists(path)

# return modification date
def ModDate(path):
	return os.path.getmtime(path)

# does a string match a pattern
def Match(pattern, str):
	return re.match(pattern, str) != None

# read a whole file
def ReadFile(path):
	f = file(path, 'r')
	s = f.read()
	f.close()
	return s

def RequireKey(title, d, key):
	if not d.has_key(key):
		print(title + " requires '" + key + "' field.")
		sys.exit(1)

def CppSourceType(str):
	return str == 'cpp' or str == 'cc' or str == 'c' or str == 'cp'

def PropogateModTimes(build_info, buildMod):
	greatestMod = buildMod
	while True:
		changes = 0
		for name, info in build_info.iteritems():
			greatestMod = max(greatestMod, info['mod'])
			for used_by in info['used_by']:
				if build_info[used_by]['mod'] < info['mod']:
					build_info[used_by]['mod'] = info['mod']
					changes += 1
		if changes == 0:
			break
	return greatestMod

def CreateBuildInfo(args, buildMod):
	build_info = {} 
	for part in args['parts']:
		name = os.path.split(part)[1];
		dot = name.rfind('.') # FIX: assumes dot was found
		part_type = name[dot+1:]
		part_name = name[:dot] 
		build_info[name] = { 'path': part, 'type': part_type, 'name': part_name, 
							 'used_by': [], 'mod': max(buildMod, ModDate(part)) }
	return build_info

def NeedBuild(product, referenceMod, flags):
	result = flags.has_key('new') or not Exists(product) or ModDate(product) < referenceMod
	return result

def RecordDependancies(build_info, type_pattern, reference_pattern):
	for name, info in build_info.iteritems():
		if Match(type_pattern, info['type']):
			includes = re.findall(reference_pattern, ReadFile(info['path']))
			for ref in includes:
				if build_info.has_key(ref):
					build_info[ref]['used_by'].append(name)

def CppApplication(args, flags, buildModDate):	
	RequireKey("CppApplication", args, 'parts')
	RequireKey("CppApplication", args, 'product')
	# set defaults
	defaults = { 'cppflags': '-Wall -Wextra -I.', 'ldflags': '', 'obj_path': './', }
	Inherit(args, defaults)

	# create info 
	build_info = CreateBuildInfo(args, buildModDate)
	objects = ''
	for name, info in build_info.iteritems():
		if CppSourceType(info['type']):
			info['dest'] = args['obj_path'] + info['name'] + '.o'
			objects += ' ' + info['dest']
		
	# find dependences
	RecordDependancies(build_info, '.*', '\#include *[\<\"](.+)[\>\"]')

	# propogate mod times
	greatestMod = PropogateModTimes(build_info, buildModDate)

	# compile objects
	for name, info in build_info.iteritems():
		if info.has_key('dest') and NeedBuild(info['dest'], info['mod'], flags):
			print('   ' + name)
			cmd = ('g++ %(cppflags)s -o ' +  info['dest'] + ' -c ' + info['path']) % args
			Run(cmd, flags.has_key('verbose'))
		
	# link app
	if NeedBuild(args['product'], greatestMod, flags):
		print('   linking...')
		cmd = ('g++ ' + objects + ' %(ldflags)s -o %(product)s') % args
		Run(cmd, flags.has_key('verbose'))
		if args['options'].has_key('gui'):
			if sys.platform == 'darwin':
				cmd = ('/Developer/Tools/Rez -t APPL -o %(product)s xcode/mac.r') % args
				Run(cmd, flags.has_key('verbose'))
		return True
	return False

def RequireTarget(targets, target):
	if not targets.has_key(target):
		print('Target, ' + target + ', not found in buildfile.')
		sys.exit(1)

def Inherit(dest, src):
	for k,v in src.iteritems():
		if not dest.has_key(k):
			dest[k] = v
		elif type(dest[k]) == type({}) and type(v) == type({}):
			Inherit(dest[k], v)

def DoBuild(target, targets, flags, options, modifications, buildModDate):
	RequireTarget(targets, target)
	for targetKey,targetValue in targets.iteritems():
		needNew = False
		if targetKey == target:
			print targetKey + '...'
			if flags.has_key('verbose') and flags['verbose']:
				print 'Building %s using flags:' % targetKey, flags, ', options:', options, ', modifications:', modifications
			defaults = { 'prefix': [], 'suffix': [], 'new': [], 'subtargets': [] }
			Inherit(targetValue, defaults)
			# build sub-projects
			for sub in targetValue['subtargets']:
				RequireKey("Targets", targets, sub)
				DoBuild(sub, targets, flags, options, modifications, buildModDate)
			# run prefix commands
			for prefix in targetValue['prefix']:
				Run(prefix, flags.get('verbose'))
			# build the target
			if targetValue.has_key('builder'):
				b = targetValue['builder']
				if not b.has_key('options'):
					b['options'] = {}
				for optKey,optValue in options.iteritems():
					b['options'][optKey] = optValue
				for modKey,modValue in modifications.iteritems():
					if b.has_key(modKey) and type(b[modKey]) == type(''):
						b[modKey] = modValue % { 'value': b[modKey] }
				RequireKey(targetKey + ' Builder', b, 'action')
				if type(b['action']) == type(DoBuild):
					needNew = b['action'](b, flags, buildModDate)
				else:
					print("Builder 'action' must be a function.")
					sys.exit(1)
				
			# run suffix commands
			for suffix in targetValue['suffix']:
				Run(suffix, flags.has_key('verbose'))

			# run after build commands
			if needNew:
				for n in targetValue['new']:
					Run(n, flags.has_key('verbose'))
		
if __name__ == "__main__":
    parser = OptionParser(usage=notice)
    parser.add_option("--new","-n",help="rebuild everything from scratch",default=False,action="store_true")
    parser.add_option('--option','-o', help="set an option passed to all builders", nargs=2, action="append")
    parser.add_option("--modify",'-m', help="modify a string parameter in all builders", nargs=2, action="append")
    parser.add_option("--build",'-b',help="set buildfile, '%s' is the default" % bfile, dest="bfile")
    parser.add_option("--verbose","-v", action="store_true", dest="verbose", default=False)
    parser.add_option("--quiet","-q", action="store_false", dest="verbose")
    # note --help is done automatically including exit
    
    (options,args) = parser.parse_args()
    
    if options.verbose: print "Options passed to script:",options
    
    if len(args) > 1: parser.error("Too many arguments")
    if len(args)==1: target = args[0]

    modifications,opts = {},{}
    if options.option:
        [opts.setdefault(opt[0],opt[1]) for opt in options.option]
    if options.modify:
        [modifications.setdefault(mod[0],mod[1]) for mod in options.modify]
    flag_args = {}  # Note changed these to use boolean instead of int.
    if options.verbose: flag_args['verbose'] = True
    if options.new: flag_args['new'] = True   
    
    execfile(bfile)
    print("--------------------")
    DoBuild(target, targets, flag_args, opts, modifications, ModDate(bfile))
    
